package controller;

import Enums.Image_Line_State;
import Enums.State;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.io.FileNotFoundException;
import javax.swing.event.ChangeEvent;
import org.apache.batik.svggen.SVGGraphics2DIOException;
import org.jgap.InvalidConfigurationException;
import tools.FileFilterImage;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Date;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JSpinner;
import javax.swing.SwingWorker;
import javax.swing.event.ChangeListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
import model.Model_Config;
import model.Model_Image_Line;
import model.Model_Image_Original;
import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2D;
import org.jfree.chart.JFreeChart;
import org.jgap.Genotype;
import org.jgap.IChromosome;
import org.jgap.impl.CompositeGene;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import tools.FileViewImage;
import tools.ShowIcon;

/**
 * Controller-Klasse. Setzt Anweisungen, die aus der view kommen um,
 * indem es z.B. das Model aendert. Bearbeitet alle Operationen, die nicht direkt mit
 * dem Evolutionsvorgang zu tun haben.
 * Als Singleton implementiert.
 */
public final class Controller implements ActionListener, ChangeListener {

    private Model_Image_Original image_original;
    private Model_Image_Line image_line;
    private Model_Config config;
    private LinePainter linePainter;
    private Thread lineThread;
    private Controller_Fitness controllerFitness;
    private PlainDocument log;
    private String dir = ".";        // beim Speichern zuletzt geoeffneter Pfad
    private String dir_picture = "."; // bild laden pfad
    private State state = State.nothing;
    private Image_Line_State lineState = Image_Line_State.saved;
    private boolean fresh_loadedProcess = false;
    //Singleton
    private static Controller instance;
    private view.GUI gui;
    private JFreeChart chart;

    private Controller() {
    }

    /**
     * Gibt die einzige Instanz des Controllers zurueck
     * (bzw. erzeugt diesen am Anfang)
     * @return das einzige Controller Objekt
     */
    public synchronized static Controller getInstance() {
        if (instance == null) {
            instance = new Controller();
        }
        return instance;
    }

    /**
     * erstellt ein Objekt der Klasse Controller_Fitness
     */
    public void createControllerFitness() {
        controllerFitness = new Controller_Fitness(config, image_original, image_line);
    }

    /**
     *
     * @param chart
     */
    public void setChart(JFreeChart chart) {
        this.chart = chart;
    }

    /**
     * setzt das Model fuer das Logfenster
     * @param model_log
     */
    public void setModelLog(PlainDocument model_log) {
        this.log = model_log;
    }

    /**
     * setzt das Model fuer das Strichbild
     * @param image_Line
     */
    public void setImage_Line(Model_Image_Line image_Line) {
        this.image_line = image_Line;
    }

    /**
     * setzt das Model fuer das Originalbild
     * @param image_Original
     */
    public void setImage_Original(Model_Image_Original image_Original) {
        this.image_original = image_Original;
    }

    /**
     * setzt das Model fuer die Einstellungswerte
     * @param config
     */
    public void setConfig(Model_Config config) {
        this.config = config;
    }

    /**
     * gui setzen
     * @param gui
     */
    public void setGUI(view.GUI gui) {
        this.gui = gui;
    }

    /**
     * Methode, die den einzelnen Events entsprechend reagiert.
     * @param e das zu behandelnde Event
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        String actionCommand = e.getActionCommand();

        if (actionCommand == Actions.laden) {
            /**
             * Wenn das Programm noch nicht gestartet wurde
             * oder all die Aenderungen in einem image_line gespeichert sind,
             * dann kann ein Bild direkt geladen werden.
             * Sonst warne den Benutzer.
             */
            if (lineState == Image_Line_State.saved) {
                loadImage();
            } else {
                loadImage_Option();
            }
        } else if (actionCommand == Actions.start) {
            start();
        } else if (actionCommand == Actions.speichern) {
            /**
             * wenn das programm läuft dann pausiere
             * vor speichern
             */
            boolean paused;

            if (linePainter!= null)
                    paused= linePainter.isPaused();
            else    paused= true;

            if (!paused)  start();

            saveProcess();

            //process fortsetzen wenn es vor dem speichern-klick am laufen war
            if(!paused)  { start(); }

        } else if (actionCommand == Actions.oeffnen) {
            /**
             * Überprüfen ob das laufende Programm gespeichert wurde.
             * Wenn nicht, dann warne den Benutzer.
             */
            if (lineState == Image_Line_State.changed) {
                loadProcess_Option();
            } else {
                loadProcess();
            }

        } else if (actionCommand == Actions.storeImage) {
            boolean paused;

            if (linePainter!= null)
                    paused= linePainter.isPaused();
            else    paused= true;

            if (!paused)  start();

            saveImage();

            //das programm fortsetzen wenn es vor dem speichern-klick am laufen war
            if(!paused) start();

        } else if (actionCommand == Actions.beenden) {
            quitProgram();
        } else if (actionCommand == Actions.logreset) {
            try {
                log.remove(0, log.getLength());
                }
            catch (BadLocationException ex) { }
        }
        else if(actionCommand==Actions.logimage){
            controllerFitness.logimage(gui);
        }
    }

    public void stateChanged(ChangeEvent e) {
        JSpinner s = (JSpinner) e.getSource();
        int interval = Integer.parseInt(""+s.getValue());
        if(interval % 10 != 0){
           interval = (interval/10)*10;
           s.setValue(interval);
        }
        controllerFitness.setloginterval(interval);
    }


    /**
     * startet den Evolutionsprozess, indem ein neuer Thread gestartet wird.
     * Dies geschieht, damit man Zwischenergebnisse speichern und den gesamten Prozess pausieren
     * kann.
     */
    public void start() {
        if (linePainter == null || !lineThread.isAlive()) {
            /**
             * wenn ein programm fortgesetzt werden muss
             */
            if(!fresh_loadedProcess)  {
                init_image_Line();
                set_Fresh_loadedProcess(false);
            }
            setState(State.running);
            linePainter = new LinePainter(image_line, controllerFitness, log, chart);
            lineThread = new Thread(linePainter);
            lineThread.start();
        } else {
            linePainter.pauseRestart();
        }
    }

    /**
     * Laedt ein Bild mit Hilfe des FileChoosers.
     * wenn es ein fehler beim laden ergibt, dann wird alles auf Sate.nothing zurückgesetzt
     */
    public void loadImage() {

        new SwingWorker() {
            /* Diesen Teil absichtlich nicht im eigenen Thread, damit das Program blockiert wird
            solange der user ein Bid waehlt. */

            private int reval;
            private JFileChooser chooser;

            {
                try {
                    chooser = new JFileChooser(dir_picture);
                    chooser.setFileFilter(FileFilterImage.getInstance());
                    chooser.setFileView(FileViewImage.getInstance());
                    chooser.setAccessory(new ShowIcon(chooser));
                    chooser.setAcceptAllFileFilterUsed(false);

                    reval = chooser.showDialog(null, "OK");

                } catch (Exception ex) {
                    JOptionPane.showMessageDialog(null, ex.getMessage() + 
                            "JFileChooser-Fehler", "ERROR",
                            JOptionPane.ERROR_MESSAGE);
                }
            }

            @Override
            protected Object doInBackground() throws Exception {

                if (reval == JFileChooser.APPROVE_OPTION) {
                   
                    BufferedImage loadImage = ImageIO.read(chooser.getSelectedFile());
                    dir_picture = chooser.getCurrentDirectory().getCanonicalPath();
                    
                  try {
                      
                      image_original.setImage_original(loadImage);

                    //controllerFitness.logtime();    // setzt neue zeit fuer bilder log
                  
                        //setzt alles was mit der Evolution zusammenhaengt auf Anfangswerte zurueck
                        //neues Bild, neue Evolution                     
                        linePainter = null;
                        Controller.getInstance().setState(State.ready);
                        setLogMessage("Bild geladen. \n");
                    } catch (Exception ex) {
                        Controller.getInstance().setState(State.nothing);
                        JOptionPane.showMessageDialog(null,"Fehler beim laden\n" 
                                + ex +"\nBitte laden Sie ein neues Bild hoch." ,
                                "ERROR", JOptionPane.ERROR_MESSAGE);
                    }
                }
                return null;
            }
        }.execute();
    }

    /**
     * setzt alles was mit der Evolution zusammenhaengt auf Anfangswerte zurueck
     * neues Bild, neue Evolution
     */
    public void init_image_Line(){
        try {
            image_line.newEvolution();
            linePainter = null;
            gui.clearPanelOriginal();
            Controller.getInstance().setLineState(Image_Line_State.saved);
        } catch (InvalidConfigurationException ex) {
           JOptionPane.showMessageDialog(null, "Fehler aufgetreten", "ERROR", JOptionPane.ERROR);
        }
    }

    /**
     * Zeigt dem Benutzer einen Dialog,
     * ob er ein neues Bild laden moechte und
     * damit alle Ungespeicherte Aenderungen verlieren gehen.
     * Wenn ja, wird loadImage() aufgerufen.
     */
    private void loadImage_Option() {
        Object[] options = {"Ja", "Nein"};
        int n = JOptionPane.showOptionDialog(
                null,
                "Wenn Sie jetzt ein Bild laden, "
                + "dann werden alle nicht gespeicherten Änderungen verworfen. \n"
                + "Möchten Sie ein neues Bild laden? ",
                "Genetische Kunst",
                JOptionPane.YES_NO_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null,
                options, options[1]);
        if (n == 0) {
            loadImage();
        }
    }

    /**
     * erlaubt Log-Nachrichten auszugeben
     * @param message die auszugebende Nachricht
     */
    public void setLogMessage(String message) {
        if (this.log != null) {
            try {
                this.log.insertString(this.log.getEndPosition().getOffset(), message, null);
            } catch (BadLocationException ex) {
                JOptionPane.showMessageDialog(null, ex.getMessage(), "ERROR", JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    /**
     * Ein Filechooser wird angezeigt.
     * Der Benutzer waehlt den Ort wo er speichern mÃ¶chte,
     * dann gibt er einen Namen fuer das Bild ein.
     */
    public void saveImage() {
        JFileChooser chooser = new JFileChooser(dir);
        java.io.File file;
        String path = "";
        boolean done = false;

        try {
            while (!done) {
                if (chooser.showSaveDialog(null) == JFileChooser.CANCEL_OPTION) {
                    done = true;
                    return;
                }
                dir = chooser.getCurrentDirectory().getCanonicalPath();

                path = chooser.getSelectedFile().getCanonicalPath();
                file = chooser.getSelectedFile(); // falls eine Datei gewaehlt wurde
                java.io.File f2 = new java.io.File(file.getCanonicalFile() + ".svg"); // nur Namen eingegeben

                if (file.exists() || f2.exists()) {
                    int confirm = JOptionPane.showConfirmDialog(chooser, file.getName() + " ist bereits vorhanden\nüberschreiben?", "Datei existiert bereits", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE);
                    switch (confirm) {
                        case JOptionPane.NO_OPTION:
                            break;
                        case JOptionPane.YES_OPTION:
                            done = true;
                            path = file.getCanonicalPath();

                            String endung= FileFilterImage.getInstance().getEnd(file);
                            if(endung =="")
                                    path += ".svg";

                            break;
                    }
                } else { // Datei existiert noch nicht, '.svg' anhaengen.
                    path += ".svg";
                    done = true;
                }
            }
            file = new java.io.File(path);
            file.setLastModified(new Date().getTime());
            //ImageIO.write(image_line.getFittestImage(), "png", file);

            saveSVGImage(path);

        } catch (Exception ex) {
            JOptionPane.showMessageDialog(null, "Es konnte nicht gespeichert werden\n" + ex.getMessage(), "Fehler beim Speichern", JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * speichert ein SVG-Bild des aktuell fittesten Chromosoms an der Stelle path
     * @param path der Pfad der zu speichernden Daeti
     * @throws FileNotFoundException
     * @throws SVGGraphics2DIOException
     */
    private void saveSVGImage(String path) throws FileNotFoundException, SVGGraphics2DIOException {
        // Get a DOMImplementation.
        DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation();

        // Create an instance of org.w3c.dom.Document.
        String svgNS = "http://www.w3.org/2000/svg";
        Document document = domImpl.createDocument(svgNS, "svg", null);

        // Create an instance of the SVG Generator.
        SVGGraphics2D svgGenerator = new SVGGraphics2D(document);

        IChromosome fittestChromosome = image_line.getGenotype().getFittestChromosome();
        CompositeGene gen;
        int x1, y1, x2, y2, stroke;
        float composite;

        for (int i = 0; i < fittestChromosome.getGenes().length; i++) {
            gen = (CompositeGene) fittestChromosome.getGene(i);
            x1 = (Integer) gen.geneAt(0).getAllele();
            y1 = (Integer) gen.geneAt(1).getAllele();
            x2 = (Integer) gen.geneAt(2).getAllele();
            y2 = (Integer) gen.geneAt(3).getAllele();
            stroke = (Integer) gen.geneAt(4).getAllele();
            composite = (((Integer) gen.geneAt(5).getAllele()) / 100f);
            //Setzt die Transparenz der Striche und sorgt dafür, dass mehrere Striche übereinander sich in der Farbe verstärken
            svgGenerator.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, composite));
            svgGenerator.setStroke(new BasicStroke((float) stroke));
            svgGenerator.draw(new Line2D.Double(x1, y1, x2, y2));
        }
        // Finally, stream out SVG UTF-8 encoding.
        boolean useCSS = true; // we want to use CSS style attributes

        FileOutputStream o = new FileOutputStream(path);
        Writer out = new OutputStreamWriter(o);//, "UTF-8");
        svgGenerator.stream(path);
        
        setLogMessage("Bild gespeichert. \n");
    }

    /**
     *  Die Methode speichert den laufenden Prozess
     *  unter dem Namen und Ort, den der User waehlt.
     */
    public boolean saveProcess() {
        return SaveLoadData.getInstance().save();
    }

    /**
     * Einen Prozess laden.
     */
    public void loadProcess() {
            SaveLoadData.getInstance().loadData();
    }

    /**
     * Bevor eine Datei geoeffnet wird, wird der Benutzer gewarnt,
     * dass nicht gespeicherte Aenderungen verloren gehen werden,
     * wenn er eine neue Datei laedt.
     */
    private void loadProcess_Option() {
        Object[] options = {"Ja", "Nein"};
        int n = JOptionPane.showOptionDialog(
                null,
                "Wenn Sie jetzt einen Prozess laden, "
                + "dann werden alle nicht gespeicherte Änderungen verworfen. \n"
                + "Möchten Sie jetzt einen Prozess laden? ",
                "Genetische Kunst",
                JOptionPane.YES_NO_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null,
                options, options[1]);
        if (n == 0) {
            loadProcess();
        }
    }

    /**
     * Die Methode schließt und beendet das Programm
     *
     * Zunaechst wird geprueft ob image_Line geaendert und nicht gespeichert wurde.
     * Wenn ja, dann wird der user gefragt ob er vorher speichern moechte.     *
     */
    public void quitProgram() {
        //Pruefen ob Aenderungen in image_Line aufgetreten sind
        // wenn ja dann erscheint ein Optionpane mit 3 buttons (Ja,Nein,Abbrechen)
        if (lineState == Image_Line_State.saved) {
            System.exit(0);
        } else if (lineState == Image_Line_State.changed || state == State.running) {
            int n= JOptionPane.showConfirmDialog(null, "Möchten Sie die Änderungen speichern, bevor Sie schließen?", "Genetische Kunst", JOptionPane.YES_NO_CANCEL_OPTION);

            switch(n){
                case JOptionPane.YES_OPTION:
                    if(saveProcess()){
                        System.exit(0);
                    }
                    break;
                case JOptionPane.NO_OPTION:
                    System.exit(0);
                    break;
                case JOptionPane.CANCEL_OPTION:
                    break;
            }
        }
    }

    /**
     * setzt den Status des image_line's auf lineState
     * @param lineState
     */
    public void setLineState(Image_Line_State lineState) {
        this.lineState = lineState;
    }

    /**
     * setzt den Status des Programmes auf state.
     * dann wird die methode updateButtons(State state) in Gui aufgerufen
     * und somit die buttons entsprechend den Status "state" geaendert     *
     * @param state
     */
    public void setState(State state) {
        this.state = state;
        gui.updateButtons(state);
        if (state == State.ready) {
            setLineState(Image_Line_State.saved);
        } else if (state == State.waiting || state == State.running) {
            setLineState(Image_Line_State.changed);
        }
    }

    /**
     * gibt den Status des Programmes zurueck
     *
     * @return state
     */
    public State getState() {
        return this.state;
    }

    /**
     * gibt den Status von image_Line zurueck
     *
     * @return lineState
     */
    public Image_Line_State getLineState() {
        return this.lineState;
    }

    /**
     *
     * @return das Model des Image_Line
     */
    public Model_Image_Line getImage_Line() {
        return this.image_line;
    }

    /**
     *
     * @return das Model Image_Original
     */
    public Model_Image_Original getImage_original() {
        return this.image_original;
    }

    /**
     *
     * @return das Model der Einstellungen
     */
    public Model_Config getConfig() {
        return this.config;
    }

    /**
     * setzt genotype in image_line mit den aktuellen eingegebenen genotype
     * @param genotype
     */
    public void setGenotype(Genotype genotype) {
        image_line.setGenotype(genotype);
    }

    /**
     * Das Model Model_Image_Original wird aufgerufen
     * und dort setImage_original() aufgerufen.
     *
     * Somit wird das originalbild geaendert und es wird im Panel neu gezeichnet.
     * @param bufferedImageOriginal
     */
    void setImage_original(BufferedImage bufferedImageOriginal) throws Exception {
        image_original.setImage_original(bufferedImageOriginal);
    }

    /**
     * Model_Config wird aufgerufen, um es mit den neuen werten von das eingegebenen
     * config zu belegen.
     * Die neuen werten sind: linesPerImage und imagesPerGeneration
     *
     * schließlich wird dies (config) an gui2 weitergegeben.
     * @param config
     */
    void changeConfig(Model_Config config) {
        this.config.setLinesPerImage(config.getLinesPerImage());
        this.config.setImagesPerGeneration(config.getImagesPerGeneration());

        gui.changeModelConfig(this.config);
    }

    void setMaske(BufferedImage maske) {
        image_original.setMaske(maske);
    }

    public void set_Fresh_loadedProcess(boolean bool){
        this.fresh_loadedProcess= bool;
    }
}
